import os
import difflib
import json
import copy



class FileType(object):
    """ Represents a generic file type.
        It should be inherited to create a specific file type::
        
                            FileType
                                |
                           ----- -----
                           |         |
                     C_FileType   H_FileType     
                     
    """
    
    instance = None
    

    """ Method that returns always the same instance of the class.
        Is a design pattern called singleton
        
        .. warning::
            This way of implementing singleton is **not** thread-safe.
    """
    def __new__(cls, *args, **kargs):
        if cls.instance is None:
            cls.instance = object.__new__(cls)
        return cls.instance
        
    def __init__(self, name, extension):
        self._extension = extension
        self._name = name

    @property
    def extension(self):
        return self._extension

    @property
    def name(self):
        return self._name
    
    def pretty_print(self, text):
        """ Return a pretty representation of the file 
            :param text: The original text to be pretty printed
        """
        return text
    



class Storage(object):
    """ Storage of project files. 
        It has tree attributes ::
        
            base_dir = The path to the directory where will be the files
            dir_name = The name of the directory
            _files = A dictionary where all the files are stored. 
        
        An example of this dictionary that contains two files: ::
        
                {
                  'main.c' : [object of C_FileType, "#include <stdio.h>..."],
                  'main.h' : [object of H_FileType, "#ifndef MAIN_H..."],
                } 
                
        Usage example::
        
            cFile = C_FileType()
            s1 = SourceStorage.Storage (config.WORKDIR,'/prueba')
            s1.addFile ('prueba', cFile)
            s1.append('prueba', cFile, '#include "<stdio.h>"\\nint main(){\\n   return 0;\\n}')
            
    """

    def __init__(self, base_dir, dir_name):
        """
            :param base_dir: path to the directory where will be the files
            :param dir_name: The name of the directory
        """
        self._files = {}
        self.base_dir = base_dir
        self.dir_name = dir_name

    def _buildDir(self):
        """ Build the source directory
        """
        if not os.access(self.base_dir + '/' + self.dir_name, os.F_OK):
            os.makedirs(self.base_dir + '/' + self.dir_name)
        if not os.access(self.base_dir + '/' + self.dir_name, os.F_OK | os.R_OK | os.W_OK):
            raise Exception


    def addFile(self, name, file_type):
        """ Add a new file to the storage

            :param name: Name of the file
            :param file_type: Instance of FileClass, specifying the type of the file
        """
        if self._files.has_key(name + file_type.extension):
            print " File already exists "
            return False
        else:
            self._files[name + file_type.extension] = [file_type, "",]
            return True
        
    def existsElement (self, name):
        """ Return true if exists the specified element
        
            :param name: The name of the stored file
        """
        if (self._files.has_key(name)):
            return True
        else:
            return False    
        
    def getElement (self, name):
        """ Get the specified file
        
            :param name: The name of the stored file
            :returns: A list with two objects: 
            
                1. An object that represents the type of the file
                2. The text of the element
        """
        if (not self.existsElement(name)):
            return False
        else:
            return self._files[name]
        
    def getNumberOfElements (self):
        """ Get the number of elements
        """
        return len(self._files)
    
    def getAllNames (self):
        """ Get the name of all the stored elements
        """
        return self._files.keys()
            
    def isTheSame(self, sourceStorage):
        """ Compare two source storages. If are equals the function returns true and
            a blank line. 
            If are different the function returns false and a line with the differences
            in a format like the diff from Linux
            
            :param sourceStorage: The other object to be compared with
            :returns: An array with this structure ::
            
                [False, []]
                [True, [[name_of_the_file, differences]]]
        """
        thisElements = set(self.getAllNames())
        otherElements = set(sourceStorage.getAllNames())
        allElements = list(thisElements | otherElements)
        emptyFile = ''
        
        are_equals = True
        differences = []
        
        # Compare each file
        for file in allElements:
            if not self.existsElement(file):
                file1 = emptyFile
            else:
                file1 = self.getElement(file)[1]
            if not sourceStorage.existsElement(file):
                file2 = emptyFile
            else:
                file2 = sourceStorage.getElement(file)[1]
            lines1 = file1.splitlines()
            lines2 = file2.splitlines()
        
            d = difflib.Differ()
            diff = list(d.compare(lines1, lines2))
        
            if len([elem for elem in diff if elem[0] != ' ']) != 0:
                are_equals = False
                # The differences are concatenated in the array preceded 
                # by the name of the file  
                differences.append ([file, diff])
            
        return [are_equals, differences]
    
    def overwrite(self, name, file_type, string):
        """ Overwrite the specified file

                :param name: Name of the file
                :param file_type: FileType instance
                :param string: String to be added
        """
        if not self._files.has_key(name + file_type.extension):
            print " File not found "
            return False
        else:
            self._files[name + file_type.extension][1] = string
            return True
        
    def append(self, name, file_type, string):
        """ Add a string to the specified file

                :param name: Name of the file
                :param file_type: FileType instance
                :param string: String to be added
        """
        if not self._files.has_key(name + file_type.extension):
            print " File not found "
            return False
        else:
            self._files[name + file_type.extension][1] += string
            return True

    def closeStorage(self):
        """ Close the storage and write the files
        """
        self._buildDir()
        for key in self._files:
            filename = self.base_dir + '/' + self.dir_name + "/" + key
            tmp_file = open(filename, 'w')
            tmp_file.write(self._files[key][1])
            tmp_file.close()
        return True
    
    def _test_Path (self, path, file):
        """
            Prepared the path to write the Storage object's files 
            
            :param path: The path to the file
            :param file: The file that is tested
        """
        if not os.access(path, os.F_OK):
            os.makedirs(path)
        if not os.access(path, os.F_OK | os.R_OK | os.W_OK):
            raise Exception
        if path[-1] == '/':
            file = path + file
        else:
            file = path + '/' + file
        return file
          
    def freeze(self, freeze_path, freeze_file):
        """ Freeze the object in a json file
        
            :param freeze_path: The path to the file
            :param freeze_file: The file that will be contain the serialized objects
        """
        file = self._test_Path(freeze_path, freeze_file)

        with open(file, 'w') as f:
            json.dump(self, f, default=_to_json, indent=2)
            
    def copy (self, element):
        """
            Make a proper copy of a Storage object
            
            :param element: The object to be copied from
        """
        self._files = copy.deepcopy(element._files)
        self.base_dir = element.base_dir
        self.dir_name = element.dir_name
    
    def defrost(self, freeze_path, freeze_file):
        """ Defrost the object from a json file
        
            :param freeze_path: The path to the file
            :param freeze_file: The file that contains the serialized objects
        """
        file = self._test_Path(freeze_path, freeze_file)
        with open(file, 'r') as f:
            element = json.load(f, object_hook=_from_json)
        # The element defrosted must be copied into self 
        self.copy (element)
        return self

        
    def __del__(self):
        #self.prettyPrint()
        self.closeStorage()


    def __str__(self):
        return '\n'.join([str(elem) + '( ' + self._files[elem][0].name + ' )' for elem in self._files])
    
    
def _to_json (python_object):
    """
        Method to frost instances of FileTypes and Storages
    """
    if isinstance(python_object, FileType):
        return {'__class__' : 'FileType',
                '__value__' : [python_object._name, python_object._extension]}
    if isinstance(python_object, Storage):
        return {'__class__' : 'Storage',
                '__value__' : [python_object._files, python_object.base_dir, python_object.dir_name]}
    raise TypeError(repr(python_object) + ' is not JSON serializable')

def _from_json (json_object):
    """
        Method to defrost instances of FileTypes and Storages
    """
    if '__class__' in json_object.keys():
        if json_object['__class__'] == 'FileType':
            return FileType (json_object['__value__'][0],json_object['__value__'][1])
        if json_object['__class__'] == 'Storage':
            st = Storage (json_object['__value__'][1],json_object['__value__'][2])
            st._files = json_object['__value__'][0]
            return st 
    return json_object
